Skip to content

Feat: flux2 dev support - #9234

Open
Pfannkuchensack wants to merge 48 commits into
invoke-ai:mainfrom
Pfannkuchensack:feature/flux2-dev-support
Open

Feat: flux2 dev support#9234
Pfannkuchensack wants to merge 48 commits into
invoke-ai:mainfrom
Pfannkuchensack:feature/flux2-dev-support

Conversation

@Pfannkuchensack

@Pfannkuchensack Pfannkuchensack commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds end-to-end support for FLUX.2 [dev] alongside the existing FLUX.2 Klein implementation. Dev uses a Mistral Small 3 text encoder (hidden 5120 → joint_attention_dim 15360, hidden states sampled at layers 10/20/30) instead of Klein's Qwen3. It shares the 32-channel AutoencoderKLFlux2 VAE and the 4D-RoPE sampling backend with Klein, so most existing infrastructure is reused — only the Mistral encoder loaders/configs and the Dev graph wiring are new.

Two Mistral encoder packagings are auto-detected: the 40-layer Mistral that ships in the black-forest-labs/FLUX.2-dev diffusers repo, and the 30-layer cow-mistral3-small distillation (Comfy-Org safetensors, gguf-org GGUFs). Single-file and GGUF encoders embed the Tekken tokenizer, so no separate tokenizer download is needed.

What works

  • Generation modes: txt2img, img2img, inpaint, outpaint (canvas)
  • Multi-reference image editing via the FLUX Kontext ref-image UI (same as Klein)
  • LoRAs for FLUX.2 [dev]
  • Model formats: diffusers folder, single-file safetensors, and GGUF transformers; Mistral encoder as diffusers / single-file (bf16 / fp8 / fp4) / GGUF, with standalone VAE + encoder selection in Advanced Settings
  • Metadata recall / Remix: Dev VAE + Mistral encoder restore correctly, disambiguated from Klein's VAE / Qwen3 slice
  • Starter models for the Dev transformer, VAE, and Mistral encoders

Limitations / notes

  • Memory: the Mistral encoder is large (fp8 ~18 GB, bf16 ~35 GB) — plan VRAM/RAM accordingly.
  • Encoder precision matters: very low-bit GGUF Mistral encoders (e.g. Q2/Q3) noticeably weaken prompt adherence; prefer fp8 / bf16 or Q6/Q8.
  • Non-BFL Mistral: upstream Mistral Small 3.1 / 3.2 (40-layer, non-BFL) is accepted but off-distribution for FLUX.2 and logs a load-time warning; it is not offered as a starter model.
  • Tokenizer fallback: encoders that don't embed Tekken (older cow GGUFs, bare diffusers folders without tokenizer/) fall back to fetching black-forest-labs/FLUX.2-dev:tokenizer from HF; offline with no HF cache this errors with documented workarounds.
  • License: FLUX.2 [dev] is Non-Commercial (flagged in isNonCommercialMainModelConfig).
  • Applying a Klein LoRA on a Dev model logs a variant-mismatch warning (no crash).

How to test

Automated:

# backend (fast, no model load)
uv run --extra cuda pytest tests/test_imports.py tests/model_identification/test_identification.py

pyproject.toml gained mistral-common; uv.lock is regenerated (uv lock --locked passes).

End-to-end:

  1. In the Model Manager install a Dev transformer (a gguf-org/flux2-dev-gguf quant — ~20 GB for Q4_K_M — or the full black-forest-labs/FLUX.2-dev diffusers folder), the FLUX.2 VAE, and a Mistral encoder (Comfy-Org fp8 recommended, or a Q6/Q8 cow GGUF).
  2. Select the Dev transformer. Advanced Settings should show FLUX.2 [dev] VAE and FLUX.2 [dev] Mistral Encoder dropdowns (not Klein's Qwen3 selector). A missing VAE/encoder should block queueing with a Dev-specific message.
  3. Defaults: 28 steps, guidance 3.5, CFG 1.0, 1024×1024. Generate — the first run with Comfy / cow files logs Loaded embedded Tekken tokenizer from <file> (no HF fetch).
  4. Exercise img2img, inpaint / outpaint on canvas, and a multi-reference edit.
  5. Apply a FLUX.2 [dev] LoRA and generate.
  6. Recall: open a generated Dev image → Remix; Dev VAE + Mistral encoder should restore. Repeat with a Klein image and confirm Klein VAE + Qwen3 still recall (no cross-contamination).

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Tests added / updated (if applicable)
  • ❗Changes to a redux slice have a corresponding migration
  • Documentation added / updated (if applicable)
  • Updated What's New copy (if doing a release after this PR)

Closes #8668

Adds end-to-end support for FLUX.2 [dev] alongside the existing Klein
implementation. Dev uses Mistral Small 3.1 (24B) as its sole text encoder
instead of Klein's Qwen3, with joint_attention_dim=15360 and the
guidance-distilled 32B transformer.

Backend
- taxonomy: Flux2VariantType.Dev, ModelType.MistralEncoder,
  ModelFormat.MistralEncoder, MistralVariantType
- configs: probe dev via context_in_dim=15360 (main + LoRA); new
  mistral_encoder.py with Diffusers / Checkpoint / GGUF configs;
  Main_Diffusers_Flux2_Config accepts Flux2Pipeline class name
- loaders: new mistral_encoder.py (AutoModel for Diffusers folder,
  MistralModel for single-file + GGUF with llama.cpp key conversion).
  Existing Klein transformer loaders are generic enough for dev
- ModelRecordChanges.variant union extended with MistralVariantType

Invocations
- flux2_dev_model_loader, flux2_dev_text_encoder (Mistral chat-template
  with FLUX2_DEV_SYSTEM_MESSAGE and layer-stacking 10/20/30),
  flux2_dev_lora_loader (+ collection variant)
- MistralEncoderField on model.py; flux2_denoise / flux2_vae_decode /
  flux2_vae_encode reused unchanged (already model-agnostic)

Frontend
- types/hooks/selectors for MistralEncoder, isFlux2DevMainModelConfig,
  selectFlux2DevDiffusersModels, useMistralEncoderModels
- params slice fields flux2DevVaeModel / flux2DevMistralEncoderModel /
  flux2DevSourceModel + reducers, selectIsFlux2Dev / selectIsFlux2Klein
- ParamFlux2DevModelSelect component, wired into AdvancedSettingsAccordion
- buildFLUXGraph dev branch with full txt2img / img2img / inpaint /
  outpaint + multi-reference image editing (same flux_kontext +
  collect chain as Klein, since Flux2RefImageExtension is model-agnostic)
- addFlux2DevLoRAs helper for dev LoRA wiring
- zModelType / zModelFormat / zFlux2VariantType extended for
  mistral_encoder / mistral_small_3_1 / dev
- OpenAPI schema regenerated, TS types updated

Starter models
- FLUX.2 [dev] Diffusers (bf16 + NF4), three GGUFs (Q4/Q6/Q8), Mistral
  encoder (bf16 + NF4)
Follow-up fixes after first end-to-end run with FLUX.2 [dev] GGUF +
Mistral 3.x GGUF + standalone FLUX.2 VAE.

Frontend
- buildFLUXGraph: wire dev model loader's vae into both flux2_denoise
  (required for BN statistics / inpaint) and flux2_vae_decode; missing
  edge was raising RequiredConnectionException at runtime
- readiness.ts: variant-aware FLUX.2 readiness check — dev requires
  flux2DevVaeModel + flux2DevMistralEncoderModel (or a Dev diffusers
  source); Klein keeps Qwen3/VAE check. Threads
  hasFlux2DevDiffusersSource through generate + canvas tabs and updates
  buildGenerateTabArg / buildCanvasTabArg test helpers
- en.json: noFlux2DevVaeModelSelected, noFlux2DevMistralEncoderModelSelected

Mistral encoder loader (GGUF / single-file)
- Fix "Cannot copy out of meta tensor": llama.cpp conversion produced
  `model.*` keys but loader instantiated bare MistralModel (no `model.`
  prefix). Add _convert_for_bare_mistral_model to strip the prefix and
  drop lm_head before load_state_dict
- _materialize_remaining_meta_tensors: after load_state_dict, replace any
  still-meta parameters (norms→ones, others→zeros) and buffers so the
  cache→VRAM move can't fail on partial state dicts, with a warning
  listing what was missing
- llama.cpp converter: map attn_q_norm/attn_k_norm (Mistral 3.x qk-norm
  variants), with ordering before attn_q/attn_k to avoid bad rewrites

Tokenizer / processor fallback
- _load_processor_with_offline_fallback walks a list of sources
  (black-forest-labs/FLUX.2-dev tokenizer subfolder, then
  mistralai/Mistral-Small-3.1-… and 3.2-…), trying AutoProcessor then
  AutoTokenizer for each, cache-first then online. Final error spells
  out the three workarounds (install Diffusers folder, set HF_ENDPOINT,
  pre-cache the tokenizer)
- flux2_dev_text_encoder: try multimodal `[{type, text}]` chat template
  first (PixtralProcessor / Mistral3Processor), fall back to plain
  string content (AutoTokenizer), then to manual [INST]…[/INST]

Qwen3 encoder probe strictness
- _get_qwen3_variant_from_state_dict and _get_variant_from_config now
  return None / raise NotAMatchError for unknown hidden_size instead of
  silently defaulting to qwen3_4b. The old fallback meant any llama.cpp
  GGUF causal LM (Mistral, Llama, …) was wrongly classified as Qwen3 —
  visible when the Mistral 3.x GGUF was identified as a Qwen3-4B encoder
- Checkpoint / GGUF / Diffusers loaders propagate the strictness
@github-actions github-actions Bot added python PRs that change python files invocations PRs that change invocations backend PRs that change backend files services PRs that change app services frontend PRs that change frontend files labels May 25, 2026
@lstein lstein self-assigned this May 27, 2026
@lstein lstein added the 6.14.0 label May 27, 2026
@lstein lstein moved this to 6.14.x Theme: USER EXPERIENCE in Invoke - Community Roadmap May 27, 2026
…andlers

Upstream Mistral Small 3.1/3.2 (40 layers) produces off-distribution embeddings
under FLUX.2's static (10, 20, 30) hidden-state extraction. The joint attention
was actually trained against BFL's 30-layer cow-mistral3-small distillation —
both Comfy-Org's safetensors and gguf-org's cow GGUFs ship the same 30-layer
weights, just packaged differently.

- Probing (configs/mistral_encoder.py) now rejects non-cow Mistrals across all
  three formats (Diffusers / Checkpoint / GGUF) with a clear error.
- Loader (load/model_loaders/mistral_encoder.py) extracts the embedded Tekken
  tokenizer from the `tekken_model` U8 (safetensors) / fp16-per-byte (cow GGUF)
  tensor via mistral_common, falling back to the BFL HF tokenizer. Removes the
  INVOKEAI_MISTRAL_TOKENIZER_SOURCE env var.
- Starter models: drop upstream Mistral 3.x entries, add Comfy-Org bf16/fp8/fp4
  variants alongside the cow GGUFs.
- MistralVariantType: drop Small3_1, keep only Cow.
- pyproject.toml: add mistral-common dependency.

Frontend recall:
- Add Flux2DevVAEModel + Flux2DevMistralEncoderModel handlers, disambiguating
  Klein vs dev via presence of `mistral_encoder` / `qwen3_encoder` metadata
  fields (both bases are `flux2`).
- Wire both into the Recall Parameters panel (hardcoded list was missing them).
- Add `metadata.mistralEncoder` i18n key + colocated tests.
@github-actions github-actions Bot added Root python-deps PRs that change python dependencies labels Jun 6, 2026
…encoders

After studying ComfyUI's `Flux2Tokenizer` / `Mistral3_24BModel` reference
implementation, align the FLUX.2 [dev] text-encoder path with their setup:

- Probing now accepts both 30-layer (cow distillation) and 40-layer (Mistral
  Small 3, BFL canonical / upstream) Mistrals. Re-adds `MistralVariantType.Mistral24B`
  alongside `Cow`. All three configs (Diffusers / Checkpoint / GGUF) updated.

- Loaders strip `model.norm` (replace with Identity) when the loaded weights
  are the 30-layer cow distillation. Matches Comfy's `final_norm=False` for
  the pruned variant; for transformers' `MistralModel` the final RMSNorm is
  always built but the cow was trained against the raw post-layer-29 state.

- 40-layer loads now log a clear warning that upstream Mistral 3.1 / 3.2 is
  NOT what FLUX.2's joint attention was trained against and recommends the
  Comfy-Org bf16/fp8/fp4 or gguf-org cow GGUF variants. BFL's canonical
  bundled text_encoder is also 40-layer so we don't hard-reject; the warning
  is opt-in self-discipline.

- Text encoder invocation switches from `apply_chat_template(messages, ...)`
  to a raw text template `[SYSTEM_PROMPT]{sys}[/SYSTEM_PROMPT][INST]{prompt}[/INST]`
  fed straight to the tokenizer — byte-for-byte matches Comfy's
  `Flux2Tokenizer.llama_template.format(text)`. System prompt now includes
  the literal `\n` between "object" and "attribution" Comfy ships.

- `_TekkenChatTemplateAdapter` renamed to `_TekkenRawTextAdapter` and exposes
  a `__call__(text, padding_side='left', ...)` interface that Tekken-encodes
  the raw string (BOS=1, no EOS) and left-pads with token id 11. Matches
  Comfy's `pad_left=True` / `pad_token=11` settings.

Frontend types extended for the new `mistral3_24b` variant
(zMistralVariantType, MODEL_VARIANT_TO_LONG_NAME, schema.ts).

@lstein lstein left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frontend build is failing. pytests are failing.

lstein and others added 5 commits July 9, 2026 20:52
Knip reported 6 unused exports. Each was dead code rather than incomplete
wiring, verified against the actual consumers:

- Drop the vestigial `flux2DevSourceModel` param end-to-end (state field,
  default, migration, reducer, action, selector, test). The FLUX graph
  builder auto-picks the diffusers source itself and never read this param;
  no UI set it. Mirrors how the Klein path already works.
- Delete `selectIsFlux2Klein`; the graph builder computes this locally and
  only `selectIsFlux2Dev` is consumed.
- Un-export `zMistralVariantType`; used only in the local `zAnyModelVariant`
  union, like `zQwenImageVariantType`.
- Delete `selectMistralEncoderModels`; components use the
  `useMistralEncoderModels` hook instead.
- Un-export `isFlux2DevMainModelConfig`; used only within types.ts, like its
  `isFluxDevMainModelConfig` / `isFlux2Klein9BMainModelConfig` siblings.
@lstein

lstein commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

I've re-verified the branch at 75e9a64 against my review. All 10 findings are correctly fixed, and the cleanup follow-ups from the review body (shared flux2_variant.py tables, loaders keyed on config.variant, single-pass GGUF read, merged flux2VaeModel slot + v3→v4 migration, deduplicated addFlux2Features builder, torch.cat) were addressed as well. Per-finding verification:

  1. Regional guidance (dev) — dev conditioning now routes through posCondCollect; the shared addFlux2Features helper makes addRegions reachable for both variants, and addRegions builds proper flux2_dev_text_encoder regional nodes wired to the Mistral encoder. ✅
  2. Transformer-only checkouts — probe now requires model_index.json or transformer/ before matching as a main pipeline. ✅
  3. Klein LoRAs in dev graph — frontend filters by config variant on both dev and Klein sides; documented fail-open for unknown variants with the backend raise as backstop. ✅
  4. Llama-2-13B GGUF false positive — vocab-size floor (100k) read from the embedding tensor in torch dim order; rejects Llama-2 (32k vocab) while admitting padded Tekken vocabs. ✅
  5. text_encoder.-prefixed checkpoints_normalize_probe_key mirrors the loader's prefix list and single-strip semantics across all three probe helpers. ✅
  6. fp32 dequant peak — per-tensor dequant with immediate cast to target dtype; lm_head dropped before the cast loop. (Nit, non-blocking: an fp8 lm_head weight_scale would still be dequantized once before the drop — bounded single-tensor transient.) ✅
  7. Silent extraction-layer fallback — replaced with a RuntimeError, diffusers-pipeline path explicitly covered, stale comment gone. ✅
  8. VAE recall — merged Flux2VAEModel handler needs only metadata.vae + base check; no dependency on resolving the main model; migration test covers the slot merge. ✅
  9. i18n keymodelManager.mistralEncoder added. ✅
  10. LoRA mismatch warning — unconditional ValueError in both single and collection loaders, symmetric dev/Klein. ✅

Only test-coverage note: the dev branch of addRegions has no dedicated unit test (it's mocked in buildFLUXGraph.test.ts). Not a blocker.

Resolving the review conversations now; I'll do one more full review pass on the updated branch before signing off.

🤖 Generated with Claude Code

@lstein lstein left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 2 review at 75e9a64. First, thank you — all 10 round-1 findings were fixed correctly, and the dedup follow-ups came out very clean (details in my earlier comment). This pass covers the fix commits plus a fresh sweep of the whole branch.

4 blockers (🔴) and 6 nice-to-haves (🟡) as inline comments. The one to look at first is the v3→v4 persist migration: as written it wipes the entire params slice for every upgrading user — the fix is one line plus a test-fixture correction.

Cleanup notes (non-blocking, take or leave):

  • configs/main.py:867 + :893-900 — with the new layout guard in place, the Flux2Transformer2DModel class-name entry and the root-config.json variant fallback ("loose transformer-only checkouts") are dead in practice; the comments now describe a layout the guard rejects. Worth dropping both and tightening the comments so nobody re-enables that path.
  • No identification tests for the three Mistral encoder probes or the new pipeline layout guard — tests/backend/model_manager/configs/test_qwen3_encoder_config.py shows the cheap synthetic-dir pattern; this probe has been the most-revised code in the PR and would benefit most.
  • load/model_loaders/mistral_encoder.py:177-189 — the three single-key GGUF metadata helpers are dead (they only call each other; production uses the batched reader). Safe to delete.
  • GGUF header is still parsed twice per text-encoder load (once in gguf_sd_loader, once in _read_gguf_metadata_values) plus once for the tokenizer submodel — each pass re-walks the 131k-token vocab arrays (~1s each). Folding the metadata read into the gguf_sd_loader pass would fix it.
  • configs/lora.py:105_FLUX2_VEC_IN_DIMS is a hand-maintained copy of flux2_variant.py's _VEC_IN_DIM values; exporting a FLUX2_VEC_IN_DIMS frozenset would close the last drift gap the shared module was created to prevent.
  • load/model_loaders/mistral_encoder.py:799-814 — the missing-norm re-init loop is fully subsumed by _materialize_remaining_meta_tensors (same init, dtype, device, norm test); its only net effect is suppressing the materialize warning on the checkpoint path while the GGUF path warns for the identical condition. Deleting it deduplicates and unifies the diagnostics.
  • _drop_quantization_metadata:352scale.repeat_interleave materializes a full weight-shape fp32 scale (3 transient copies vs 2); a reshape+broadcast multiply is a drop-in saving ~0.7-2.7GB on the largest tensors.
  • parsing.test.tsx:34-113 — the fakeMainModel/modelRegistry fixtures and the it.each variant axis are dead (the merged handler never reads metadata.model; the registry key is never looked up), and the comment claims variant disambiguation that no longer exists. The two variant cases run the same test twice.

Checked and deliberately not raised: the Klein LoRA loaders' warn→raise change without a version bump (a dev-variant LoRA config cannot exist pre-PR, so no working workflow changes behavior); the llama.cpp→transformers key converter's replace chain (the trailing-dot patterns are collision-safe and unknown keys pass through unchanged, matching the z_image converter); GGUF reader mmap lifetime on Windows (the raw readers are transient locals; the long-lived-view problem WrappedGGUFReader solves doesn't apply).

🤖 Generated with Claude Code

Comment thread invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts Outdated
Comment thread invokeai/app/invocations/flux2_dev_model_loader.py Outdated
Comment thread invokeai/backend/model_manager/load/model_loaders/mistral_encoder.py Outdated
Comment thread invokeai/app/invocations/flux2_dev_lora_loader.py Outdated
Comment thread invokeai/backend/model_manager/load/model_loaders/mistral_encoder.py Outdated
Comment thread pyproject.toml Outdated
Comment thread invokeai/app/invocations/flux2_dev_text_encoder.py Outdated
Blockers:
- params migration: seed flux2DevMistralEncoderModel in the v3->v4 step so a
  genuine v3 blob passes zParamsState.parse() instead of wiping the whole params
  slice on upgrade; rebuild the migration test fixture as a field-accurate v3
  object so it actually covers the regression.
- guidance for [dev]: resolve the image's own model in the Guidance metadata
  parse gate and exempt variant === 'dev' so guidance is displayed/recalled for
  [dev] (still skipped for Klein); render the guidance slider for FLUX.2 [dev].
- source-model variant guard: require variant == Dev where the dev loader
  validates its Mistral/VAE source, and reject a [dev] source in the Klein loader
  — a mismatched pipeline otherwise fails with an opaque matmul error in denoise.
- tokenizer offline load: drop the dead root-dir fallback + duplicated pre-try
  and add a root-directory AutoProcessor step to _load_tokenizer_for_model so
  processor files alongside the encoder weights load offline.

Cleanups:
- extract _reinit_inv_freq() with a rope_theta -> rope_parameters/rope_scaling
  fallback (fixes a latent AttributeError on pinned transformers 5.5, removes a
  verbatim duplicate).
- flux2_dev_lora_collection_loader: replace the base assert with a ValueError
  that rejects non-FLUX.2 LoRAs, mirroring the Klein collection loader.
- diffusers Mistral load: drop the never-run vision_tower/multi_modal_projector
  (~0.8GB) so they stay out of the cache and VRAM transfers.
- clear flux2DevMistralEncoderModel on base switch and intra-flux2 variant switch.
- pin mistral-common>=1.5.4,<2 (validated against 1.11.6).
- fix contradictory 40-layer docstrings to match the taxonomy/loader story.
Resolve conflicts:
- parsing.test.tsx: keep upstream's new T5EncoderModel handler tests and this
  branch's FLUX.2 [dev] guidance variant-gating tests; union the fakeModel type
  ('mistral_encoder' + 't5_encoder').
- schema.ts / openapi.json: regenerated from the merged backend so both the
  FLUX.2 [dev] and T5 encoder additions are present.
@Pfannkuchensack
Pfannkuchensack requested a review from lstein July 25, 2026 16:48
Pfannkuchensack and others added 8 commits July 28, 2026 00:10
…upport

Resolves conflicts with upstream video generation (invoke-ai#9163) and Ideogram 4
(invoke-ai#9303).

Conflict resolutions:
- Kept both new encoder types side by side (mistral_encoder for FLUX.2 [dev],
  wan_t5_encoder for Wan 2.2) across taxonomy, invocation fields, model
  manager and node types
- isNonCommercialMainModelConfig now covers FLUX dev, FLUX.2 Klein 9B,
  FLUX.2 dev and Ideogram 4
- Regenerated uv.lock from the merged pyproject (adds imageio/imageio-ffmpeg)
Brings in the round-2 review fixes and the tightened mistral-common pin on
top of the local merge of upstream video generation (invoke-ai#9163).

Conflict resolutions:
- modelSelected: kept the Wan 2.2 auto-default block alongside origin's
  reworded FLUX.2 variant-switch comment covering both encoder slots
…port

# Conflicts:
#	invokeai/app/services/model_records/model_records_base.py
#	invokeai/backend/model_manager/configs/factory.py
#	invokeai/backend/model_manager/taxonomy.py
#	invokeai/frontend/web/openapi.json
#	invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
#	invokeai/frontend/web/src/features/controlLayers/store/types.ts
#	invokeai/frontend/web/src/features/modelManagerV2/models.ts
#	invokeai/frontend/web/src/features/nodes/types/common.ts
#	invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts
#	invokeai/frontend/web/src/services/api/hooks/modelsByType.ts
#	invokeai/frontend/web/src/services/api/schema.ts
…ision

main and this branch both shipped _version 4 with different new keys (PiD
fields vs the flux2 VAE merge + Mistral encoder slot), so a v4 blob written
by either parent would fail zParamsState.parse() after the merge and wipe
the whole params slice. Keep main's v3->v4 step verbatim and move the flux2
slot merge + Mistral seed to a new v4->v5 step with conditional seeding for
both v4 shapes.

Also seed the five Wan component fields in v3->v4: they were added to the
schema without a version bump while releases were still writing v3 blobs,
so a genuine released-build (v6.13.x) v3 blob fails parse() on them today
- same wipe, inherited from main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@lstein lstein left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've re-verified the branch at 3e62aa8 against my second review. Thank you for the thorough fix commit (5026c02) — 8 of the 10 findings are correctly and completely fixed, including all the hard parts (the migration seed with a field-accurate test fixture, model-resolving guidance recall, the _reinit_inv_freq extraction validated against transformers 5.5.4, and the vision-tower drop). Two of the fixes need a follow-up, which is why this is another change request rather than an approval. Details below.

Requested changes

🔴 1. The new Klein-loader variant guard broke the VAE-only fallback (regression from the blocker-3 fix)

flux2_klein_model_loader.py calls _validate_diffusers_format on both call sites — the encoder-extraction path (:154) and the VAE-only extraction path (:132). The encoder-side rejection is exactly right, but the 32-channel AutoencoderKLFlux2 is shared between Klein and [dev], and the linear UI deliberately relies on that: buildFLUXGraph.ts falls back to any FLUX.2 diffusers pipeline when only the VAE is needed ("Fall back to any diffusers model if only the VAE is needed"), and isFlux2DiffusersMainModelConfig / readiness don't filter by variant.

Triggering sequence: install a Klein GGUF main + a standalone Qwen3 encoder + a FLUX.2 [dev] diffusers pipeline as the only FLUX.2 diffusers model (no standalone VAE) → readiness enables Invoke → the builder sets qwen3_source_model to the [dev] pipeline for VAE extraction → the new guard raises ValueError claiming the VAE is "incompatible with the Klein transformer" — which worked before this fix, and the message is factually wrong for the VAE.

Suggested fix: apply the variant guard only at the tokenizer/encoder extraction call sites ([dev] loader :153, Klein loader :154), not the VAE ones — or, if you prefer to reject cross-variant VAE sourcing on principle, the frontend readiness + source-selection fallback must be variant-filtered to match, so users get a disabled Invoke button with a reason instead of a runtime error.

🔴 2. The root-directory tokenizer step still fails offline for the exact layout blocker 4 described

The structural cleanup is all there (dead fallback gone, duplicated pre-try gone, ValueError in every except tuple, all three loaders share the helper). But the new step 3 (mistral_encoder.py:676-681) tries AutoProcessor only. With the pinned transformers 5.5.4, AutoProcessor.from_pretrained on a directory whose config.json is model_type: "mistral3" — which is precisely the BFL-style standalone-encoder layout this step targets — raises OSError: Can't load image processor … (it resolves mistral3 to a multimodal processor and requires preprocessor_config.json) before reaching any tokenizer fallback. The exception is swallowed and the ladder falls through to the HF fetch, which raises RuntimeError offline.

Reproduced empirically against the project venv with HF_HUB_OFFLINE=1: a root directory with a mistral3 config.json plus a valid tekken.json (or tokenizer.json + tokenizer_config.json) fails via AutoProcessor, while AutoTokenizer.from_pretrained on the same directory succeeds and returns a working backend. So the tokenizer is loadable — only the loader-class choice loses it. The step only succeeds today when the root config is text-only model_type: "mistral" or a full processor set including preprocessor_config.json is present.

Suggested fix: loop (AutoProcessor, AutoTokenizer) in steps 2 and 3, exactly as _load_tokenizer_from_hf already does (:616). Note if you do: probing showed AutoTokenizer can raise KeyError: 'special_tokens' on a directory containing only tekken.json, so the except tuple there needs KeyError too (or that escapes loudly).

Note on the two commits I just pushed (merge of main + migration v5 bump)

I've pushed 567cba9 (merge of latest main, which brings in the PiD decoder work — the flux2 PiD wiring is adapted into this branch's addFlux2Features helper and verified behavior-identical to main's) and d3c7bde. The second commit fixes a semantic conflict the textual merge couldn't see, and it wasn't your bug: main (after the PiD PR, 802de41) and this branch both bumped the params persist schema to _version: 4 with different new keys (main: pidMode/pidDecoderModel/gemma2EncoderModel/pidSteps, still with kleinVaeModel; this branch: flux2VaeModel + flux2DevMistralEncoderModel). After a plain merge, a v4 blob written by either parent is missing the other side's keys, skips every migration branch, fails zParamsState.parse(), and silently wipes the whole params slice — the same failure mode as blocker 1, reintroduced by the version collision.

The fix keeps main's v3→v4 verbatim and moves this branch's flux2 slot-merge + Mistral seed into a new v4→v5 step with conditional (?? null) seeding for both v4 shapes, with tests covering both. It also seeds the five wan* keys in v3→v4: main added them to the schema without a version bump while v6.13.x releases were still writing v3 blobs, so genuine released-build v3 blobs fail parse on those keys today (empirically confirmed — inherited from main, needs the same fix there).

Verified fixed (no action needed)

  1. Migration seed (blocker 1)flux2DevMistralEncoderModel = null seeded in the migration; the rebuilt fixture genuinely covers the regression (deleting the seed fails the test). ✅
  2. Guidance for [dev] (blocker 2) — the parse gate resolves the image's own model and exempts variant === 'dev'; Klein still skips; slider renders for dev only via the same predicate the graph builder uses, so visibility and consumption can't drift; generate → metadata → recall → regenerate round-trips. Uninstalled-model dev images fall back to skipping guidance — reasonable fail-safe. ✅
  3. Encoder-side variant guards (blocker 3) — the reported Klein-encoder-into-dev-transformer path (and mirror) is closed on every reachable route with clear ValueErrors, and the linear UI filters dev sources by variant. (VAE-path over-reach above is the only issue.) ✅
  4. Tokenizer structure (blocker 4) — dead fallback and duplicate pre-try removed, ValueError caught, single helper used by all three loaders. (AutoProcessor-only gap above is the only issue.) ✅
  5. Collection LoRA base checkValueError before dedup/config fetch; FLUX.1 rejected; round-1 Klein-variant rejection intact; Klein side symmetric. ✅
  6. Mistral slot clearing — cleared on base switch (with toast accounting) and intra-flux2 variant switch, including when the Klein encoder is unset; recall path covered via modelSelected. ✅
  7. rope_theta — single _reinit_inv_freq with the rope_parameters/rope_scaling fallback; verified on transformers 5.5.4 that rope_theta set via MistralConfig(rope_theta=…) lands exactly where the fallback reads it. ✅
  8. mistral-common pin>=1.5.4,<2 in the right block, lock resolves 1.11.6, and both private-API usages (_special_tokens_reverse_vocab, Tekkenizer.encode(s, bos, eos)) verified present in-range with defensive fallbacks. ✅
  9. Vision tower dropsetattr(model, …, None) removes the tower/projector from state_dict()/named_parameters(), so cache accounting and VRAM transfers exclude it; .language_model access and index-based extraction unaffected. Residual, non-blocking: the sibling full-pipeline path (flux.py Flux2DiffusersModel._load_model, used for diffusers mains and mistral_source_model) still loads and carries the ~0.8 GB tower — worth the same treatment in a follow-up. ✅
  10. 40-layer docs — all four sources now agree on the operational contract (accepted, warned, indices 10/20/30, final norm kept for 40-layer / stripped for cow). Nit, non-blocking: starter_models.py says BFL's own 40-layer has weaker adherence due to index depth, while the loader warning implies only non-BFL 40-layer weights degrade — pick one story. ✅

Pre-existing, out of scope for this PR (noting for completeness): the workflow editor can still feed a Klein 9B-family source into a 4B main (no cross-Klein variant check on the source-extraction path — same wrong-width class, linear UI is protected); the Klein loader never validates its main model's variant.

🤖 Generated with Claude Code

…n, widen the tokenizer ladder

The FLUX.2 loaders ran one validator on both the VAE- and the encoder-extraction
call site, so the cross-variant check also rejected VAE-only sourcing. Klein and
[dev] share the same 32-channel AutoencoderKLFlux2 and the linear UI relies on
that -- buildFLUXGraph falls back to any FLUX.2 diffusers pipeline when only the
VAE is needed, and readiness does not filter by variant. A Klein GGUF main plus a
standalone Qwen3 encoder plus a [dev] pipeline as the only diffusers model
therefore hit a ValueError behind an enabled Invoke button. Split the validator:
format-only for the VAE path, format + variant for the encoder path.

The Mistral tokenizer ladder's local-directory rungs tried AutoProcessor only.
On transformers 5.5.4 that raises OSError for a mistral3 config.json without
preprocessor_config.json -- exactly the BFL-style standalone-encoder layout the
rungs target -- so the ladder fell through to the HF fetch and failed offline.
Both rungs now loop (AutoProcessor, AutoTokenizer), with KeyError in the except
tuple for tekken-only directories.
@github-actions github-actions Bot added the python-tests PRs that change python tests label Jul 31, 2026
@Pfannkuchensack
Pfannkuchensack requested a review from lstein July 31, 2026 03:54
Pfannkuchensack and others added 4 commits July 31, 2026 06:15
FLUX.2 [dev] recorded its Mistral text encoder as an undeclared extra key,
relying on the node's `extra='allow'`, while the Klein counterpart
`qwen3_encoder` is a proper field. Declare it so it lands in the OpenAPI
schema and is typed in the frontend instead of `unknown`.

Also bumps the node version, which was left at 2.1.0 across several model
integrations that widened the node: `ideogram4_caption` (invoke-ai#9303) and the
generation modes for FLUX.2, Anima, Qwen-Image, Ideogram 4, Wan, Krea-2 and
Ernie. All changes are additive, so this is a minor bump - saved workflows
carrying a core_metadata node now auto-update to the current field set on
load rather than silently keeping a stale one.
Conflicts:
- addRegions.ts: adopted main's RegionalPositiveConditioning alias and added
  flux2_dev_text_encoder to it
- schema.ts/openapi.json: regenerated from the merged backend

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Main's invoke-ai#9428 marked every text-encoder node idle_gpu_offloadable and added a
registry guard asserting the marker on all *_text_encoder nodes; the merge
brought that guard onto this branch where flux2_dev_text_encoder (which
neither parent knew about) fails it.

The flag alone would be wrong: the marker's contract is that the saved
conditioning is CPU-backed, because the borrowed GPU's pool lock is released
the moment the node returns. Move the Mistral embeds to CPU before save
(the placeholder clip_embeds follows their device), add the marker, bump to
1.0.1, and add the same output-device regression test the Klein encoder has.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@lstein lstein left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 4 review (head c2b7778)

Thanks for the quick turnaround on both round-3 blockers. I re-verified each fix in code and with fresh-context adversarial passes (including mutation-testing your new tests — they do catch reverts). Summary:

  • Round-3 blocker 1 (cross-variant guard also rejected VAE-only sourcing): fixed correctly. The split is right, every source-based encoder extraction goes through _validate_encoder_source, no VAE branch retains variant logic, and the linear UI can't wire a cross-variant source into an encoder slot in any main-format × standalone-VAE × standalone-encoder combination I enumerated.
  • Round-3 blocker 2 (AutoProcessor-only local rungs): the offline gap itself is fixed — both local rungs now loop (AutoProcessor, AutoTokenizer). Unfortunately the widened AutoTokenizer rung introduces two new problems, one of which silently corrupts conditioning. Details below; both reproduced empirically against transformers 5.5.4 with real files, not hypothesized.
  • The core_metadata mistral_encoder declaration + 2.2.0 bump looks good.

Housekeeping: I pushed two commits

  • 7179712 — merge of current main. Conflicts: addRegions.ts (main refactored the posCond unions into a shared RegionalPositiveConditioning alias for Krea-2 regional prompting; I added flux2_dev_text_encoder to the alias and took main's shape), and schema.ts/openapi.json (regenerated from the merged backend rather than hand-merged).
  • c2b7778 — the merge surfaced a semantic conflict git can't see: main's #9428 added a registry guard asserting every *_text_encoder node declares idle_gpu_offloadable=True, and flux2_dev_text_encoder (which neither parent knew about) failed it, so python-tests would have gone red. The flag alone would have been wrong — the marker's contract is that saved conditioning is CPU-backed, because the borrowed GPU's pool lock is released the moment the node returns. I mirrored the Klein encoder: detach().to("cpu") before save, marker added, version 1.0.1, plus the same output-device regression test Klein has. tests/app is fully green at head (2161 passed), as are the frontend suite (1736) and lints.

Blocker 1: AttributeError escapes the ladder and crashes the whole load

_TOKENIZER_LOAD_ERRORS = (OSError, EnvironmentError, ValueError, KeyError) (mistral_encoder.py:623) doesn't cover a failure mode of the very rung you added. In transformers 5.5.4, AutoTokenizer.from_pretrained on a directory whose tokenizer_config.json names a tokenizer_class unknown to 5.5.4 hits tokenizer_class_from_name(...) → None and raises AttributeError: 'NoneType' object has no attribute 'from_pretrained' (tokenization_auto.py:723 has no None-guard).

Triggering sequence (reproduced): encoder dir with config.json (model_type: "mistral3"), tekken.json, and a tokenizer_config.json containing "tokenizer_class": "MistralCommonTokenizer" — exactly what transformers 4.5x's MistralCommonTokenizer.save_pretrained writes. Rung 3's AutoProcessor raises OSError (caught), AutoTokenizer raises AttributeError → escapes _try_load_tokenizer_from_dir_load_tokenizer_for_model crashes instead of falling through to the HF rung. Before this commit the same directory worked whenever rung 4 could succeed.

Given the ladder's whole contract is graceful fallthrough, I'd catch broadly in _try_load_tokenizer_from_dir (add AttributeError, or catch Exception and log) — an exotic exception from a probe rung should never be able to kill the load while a working fallback remains.

Blocker 2: root tekken.json + config.json now loads MistralCommonBackend, which tokenizes the template markers wrong — silently

The commit comment assumes tekken-carrying directories fail with KeyError: 'special_tokens'. That's only true when config.json is absent. With config.json (model_type: "mistral3") + a real tekken.json — the layout of an official mistralai Mistral-Small download, and the standalone-encoder layout these loaders explicitly accept — AutoTokenizer succeeds and returns MistralCommonBackend (mistral-common is a hard dependency, so this holds in every standard install).

That object BPE-encodes [SYSTEM_PROMPT]/[INST] as literal text instead of splicing them as single Tekken special ids. Verified against this module's own _TekkenRawTextAdapter on the same prompt: reference ends ..., 1, 17, 33153, 5117, 18, 3, 1097, 7990, 4 (markers = ids 17/18/3/4); MistralCommonBackend produces 1, 1091, 101289, 58343, ... (literal [, SY, STEM…). Every structural token is off-distribution, the encode "works", and generations silently degrade. Before this commit, that directory fell through to rung 4 and got the correct BFL processor whenever online/cached — so this regresses the online case too, and offline it trades an actionable RuntimeError for silent corruption.

Your ladder tests missed it because the fixture's tekken.json is a small fake that raises during parse; a real one parses fine and short-circuits.

Suggested fix: the ideal handling for a root tekken.json already exists in this file — wrap it with _TekkenRawTextAdapter (same as the embedded-tekken rung) before ever handing the directory to AutoTokenizer; and/or treat a mistral-common-backed result from AutoTokenizer as "not usable, keep falling". Either way, please add a fixture with a structurally valid tekken model file so the test exercises the success path, not just the raise path.

Non-blockers

  1. The [dev]-side justification of the guard split overclaims, and the [dev] half is UI-dead. "buildFLUXGraph falls back to any FLUX.2 diffusers pipeline when only the VAE is needed" is true only of the Klein branch. The dev branch selects from dev-only pipelines (selectFlux2DevDiffusersModels) and readiness requires a dev diffusers source, so the config your test_dev_loader_extracts_vae_from_a_klein_pipeline proves the backend accepts (dev GGUF + standalone Mistral encoder + Klein diffusers as the only VAE source) still shows a disabled Invoke button. Safe direction, but either relax the dev builder/readiness VAE-sourcing to mirror Klein or trim the docstrings to the Klein case.
  2. _validate_encoder_source in the Klein loader only rejects Dev, not Klein-family mismatches, while its comment claims the opaque-matmul class is closed. Klein 4B GGUF main + Klein 9B diffusers as qwen3_source_model (workflow editor) passes the guard and hits exactly that matmul error; the standalone-encoder path (_validate_qwen3_encoder_variant) and the frontend both enforce the family match — only the source path doesn't. Pre-existing, but worth closing while you're here: check the Klein→Qwen3 family map in the source path too. Related asymmetry: the dev guard rejects any non-dev variant, the Klein guard only dev — a future third FLUX.2 variant would be rejected on one side and silently accepted on the other.

Requesting changes for the two tokenizer-ladder blockers.

…silently mis-encoding Tekken

Round-4 review blockers, both reproduced against transformers 5.5.4 with real files before fixing.

1. An AttributeError from a probe rung killed the whole load. `AutoTokenizer.from_pretrained` on a
   directory whose `tokenizer_config.json` names a `tokenizer_class` the installed transformers does
   not know resolves that class to None and dereferences it without a guard — exactly the layout
   `MistralCommonTokenizer.save_pretrained` writes. That is not in `_TOKENIZER_LOAD_ERRORS`, so it
   escaped `_try_load_tokenizer_from_dir` and crashed a load the HF rung would have completed. The
   probes now catch broadly; the expected-error tuple only selects the log level, so an unexpected
   failure is still logged loudly instead of being swallowed.

2. A root `tekken.json` next to `config.json` did not fail in `AutoTokenizer` — it resolved to a
   mistral-common-backed tokenizer that BPE-encodes `[SYSTEM_PROMPT]`/`[INST]` as literal text
   instead of splicing them as single Tekken ids. The encode "worked" and conditioning was silently
   off-distribution. Fixed on two independent paths: the ladder now reads a standalone `tekken.json`
   itself, ahead of the transformers probes, and any mistral-common-backed result is re-wrapped in
   `_TekkenRawTextAdapter` through its underlying `MistralTokenizer` rather than used as-is. The
   vocab is fine — only its `__call__` is wrong — so re-wrapping beats discarding, which would have
   traded silent corruption for an offline RuntimeError. Verified: the re-wrapped ids are identical
   to the reference adapter's.

Also closes both non-blockers:

- `_validate_encoder_source` in the Klein loader rejected only [dev], so a Klein 9B pipeline passed
  as `qwen3_source_model` for a Klein 4B transformer and hit the very matmul error the guard exists
  to prevent (the frontend and the standalone-encoder path both enforce the family match; the
  workflow editor's source field was the only way in). It is now an allowlist keyed on a shared
  `_KLEIN_TO_QWEN3_VARIANT` map — mirroring the frontend's `KLEIN_TO_QWEN3_VARIANT_MAP` — and checks
  the source's Qwen3 family against the main model, so a future third FLUX.2 variant fails closed on
  the Klein side too, not just on [dev]. `_validate_qwen3_encoder_variant` shares that map and now
  uses `getattr` instead of `hasattr`, which turned a None variant into an AttributeError in the
  error path rather than the intended ValueError.

- The [dev] loader's `_validate_diffusers_format` docstring claimed the linear UI relies on the
  permissive VAE path. That holds for Klein, but the [dev] builder sources from dev-only pipelines
  and readiness gates on one, so there the cross-variant VAE case is reachable through the workflow
  editor only. The justification now states what actually holds: the 32-channel AutoencoderKLFlux2
  is shared (the repo ships the Klein-sourced `flux2_vae` as a dependency of every [dev] GGUF
  starter), and `mistral_source_model` is not variant-filtered in the editor.

Tests: a structurally valid Tekken fixture, so the ladder exercises the success path rather than
only the raise path the previous fake produced; regression tests for both blockers on the directory
and HF rungs; Klein-family coverage including same-family acceptance and the standalone-encoder
guard's negative path, which had no coverage at all. All new tests mutation-verified — reverting the
broad catch, the tekken rung, the re-wrap, the family check, or the allowlist each fails at least
one.

tests/app + tests/backend/model_manager: 3010 passed. The 9 failures are the pre-existing
network-dependent ones in test_model_install / test_load_api / test_download_queue.
@Pfannkuchensack
Pfannkuchensack requested a review from lstein August 6, 2026 03:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

6.14.0 backend PRs that change backend files frontend PRs that change frontend files invocations PRs that change invocations python PRs that change python files python-deps PRs that change python dependencies python-tests PRs that change python tests Root services PRs that change app services

Projects

Status: 6.14.x Theme: USER EXPERIENCE

Development

Successfully merging this pull request may close these issues.

[enhancement]: Support for Flux.2 model

2 participants